import { SuiTransaction, TokenTransferProgrammableTransaction, TransactionExplanation, TxData } from './iface';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { Transaction } from './transaction';
import { SUI_ADDRESS_LENGTH, UNAVAILABLE_TEXT } from './constants';
import {
  BaseKey,
  PublicKey as BasePublicKey,
  InvalidTransactionError,
  ParseTransactionError,
  Signature,
  TransactionRecipient,
  TransactionType,
} from '@bitgo/sdk-core';
import { CallArg, normalizeSuiAddress, SuiObjectRef } from './mystenlab/types';
import utils, { isImmOrOwnedObj } from './utils';
import {
  builder,
  Inputs,
  TransactionArgument,
  TransactionBlockInput,
  TransactionType as SuiTransactionBlockType,
} from './mystenlab/builder';
import { BCS } from '@mysten/bcs';

export class TokenTransferTransaction extends Transaction<TokenTransferProgrammableTransaction> {
  constructor(_coinConfig: Readonly<CoinConfig>) {
    super(_coinConfig);
  }

  get suiTransaction(): SuiTransaction<TokenTransferProgrammableTransaction> {
    return this._suiTransaction;
  }

  setSuiTransaction(tx: SuiTransaction<TokenTransferProgrammableTransaction>): void {
    this._suiTransaction = tx;
  }

  /** @inheritDoc */
  get id(): string {
    return this._id || UNAVAILABLE_TEXT;
  }

  addSignature(publicKey: BasePublicKey, signature: Buffer): void {
    this._signatures.push(signature.toString('hex'));
    this._signature = { publicKey, signature };
    this.serialize();
  }

  get suiSignature(): Signature {
    return this._signature;
  }

  /** @inheritdoc */
  canSign(key: BaseKey): boolean {
    return true;
  }

  /** @inheritdoc */
  toBroadcastFormat(): string {
    if (!this._suiTransaction) {
      throw new InvalidTransactionError('Empty transaction');
    }
    return this.serialize();
  }

  /** @inheritdoc */
  toJson(): TxData {
    if (!this._suiTransaction) {
      throw new ParseTransactionError('Empty transaction');
    }

    const tx = this._suiTransaction;
    return {
      id: this._id,
      sender: tx.sender,
      kind: { ProgrammableTransaction: tx.tx },
      gasData: tx.gasData,
      expiration: { None: null },
      inputObjects: this.getInputObjectsFromTx(tx.tx),
    };
  }

  /** @inheritDoc */
  explainTransaction(): TransactionExplanation {
    const result = this.toJson();
    const displayOrder = ['id', 'outputs', 'outputAmount', 'changeOutputs', 'changeAmount', 'fee', 'type'];
    const outputs: TransactionRecipient[] = [];

    const explanationResult: TransactionExplanation = {
      displayOrder,
      id: this.id,
      outputs,
      outputAmount: '0',
      changeOutputs: [],
      changeAmount: '0',
      fee: { fee: this.suiTransaction.gasData.budget.toString() },
      type: this.type,
    };

    switch (this.type) {
      case TransactionType.Send:
        return this.explainTokenTransferTransaction(result, explanationResult);
      default:
        throw new InvalidTransactionError('Transaction type not supported');
    }
  }

  /**
   * Set the transaction type.
   *
   * @param {TransactionType} transactionType The transaction type to be set.
   */
  transactionType(transactionType: TransactionType): void {
    this._type = transactionType;
  }

  /**
   * Load the input and output data on this transaction.
   */
  loadInputsAndOutputs(): void {
    if (!this.suiTransaction) {
      return;
    }

    const recipients = utils.getRecipients(this._suiTransaction);
    const totalAmount = recipients.reduce((accumulator, current) => accumulator + Number(current.amount), 0);
    this._outputs = recipients.map((recipient) => ({
      address: recipient.address,
      value: recipient.amount,
      coin: this._coinConfig.name,
    }));
    this._inputs = [
      {
        address: this.suiTransaction.sender,
        value: totalAmount.toString(),
        coin: this._coinConfig.name,
      },
    ];
  }

  /**
   * Sets this transaction payload
   *
   * @param {string} rawTransaction
   */
  fromRawTransaction(rawTransaction: string): void {
    try {
      utils.isValidRawTransaction(rawTransaction);
      this._suiTransaction = Transaction.deserializeSuiTransaction(
        rawTransaction
      ) as SuiTransaction<TokenTransferProgrammableTransaction>;
      this._type = TransactionType.Send;
      this._id = this._suiTransaction.id;
      this.loadInputsAndOutputs();
    } catch (e) {
      throw e;
    }
  }

  /**
   * Helper function for serialize() to get the correct txData with transaction type
   *
   * @return {TxData}
   */
  public getTxData(): TxData {
    if (!this._suiTransaction) {
      throw new InvalidTransactionError('empty transaction');
    }
    const inputs: CallArg[] | TransactionBlockInput[] = this._suiTransaction.tx.inputs.map((input) => {
      if (input.hasOwnProperty('Object')) {
        return input;
      }
      if (input.hasOwnProperty('Pure')) {
        if (input.Pure.length === SUI_ADDRESS_LENGTH) {
          const address = normalizeSuiAddress(
            builder.de(BCS.ADDRESS, Buffer.from(input.Pure).toString('base64'), 'base64')
          );
          return Inputs.Pure(address, BCS.ADDRESS);
        } else {
          const amount = builder.de(BCS.U64, Buffer.from(input.Pure).toString('base64'), 'base64');
          return Inputs.Pure(amount, BCS.U64);
        }
      }
      if (input.kind === 'Input' && (input.value.hasOwnProperty('Object') || input.value.hasOwnProperty('Pure'))) {
        return input.value;
      }
      return Inputs.Pure(input.value, input.type === 'pure' ? BCS.U64 : BCS.ADDRESS);
    });

    const programmableTx = {
      inputs,
      transactions: this._suiTransaction.tx.transactions,
    } as TokenTransferProgrammableTransaction;

    return {
      sender: this._suiTransaction.sender,
      expiration: { None: null },
      gasData: this._suiTransaction.gasData,
      kind: {
        ProgrammableTransaction: programmableTx,
      },
    };
  }

  /**
   * Returns a complete explanation for a transfer transaction
   * @param {TxData} json The transaction data in json format
   * @param {TransactionExplanation} explanationResult The transaction explanation to be completed
   * @returns {TransactionExplanation}
   */
  explainTokenTransferTransaction(json: TxData, explanationResult: TransactionExplanation): TransactionExplanation {
    const recipients = utils.getRecipients(this.suiTransaction);
    const outputs: TransactionRecipient[] = recipients.map((recipient) => recipient);
    const outputAmount = recipients.reduce((accumulator, current) => accumulator + Number(current.amount), 0);

    return {
      ...explanationResult,
      outputAmount,
      outputs,
    };
  }

  /**
   * Extracts the objects that were provided as inputs while building the transaction
   * @param tx
   * @returns {SuiObjectRef[]} Objects that are inputs for the transaction
   */
  private getInputObjectsFromTx(tx: TokenTransferProgrammableTransaction): SuiObjectRef[] {
    const inputs = tx.inputs;
    const transaction = tx.transactions[0] as SuiTransactionBlockType;

    let args: TransactionArgument[] = [];
    if (transaction.kind === 'MergeCoins') {
      const { destination, sources } = transaction;
      args = [destination, ...sources];
    } else if (transaction.kind === 'SplitCoins') {
      args = [transaction.coin];
    }

    const inputObjects: SuiObjectRef[] = [];
    args.forEach((arg) => {
      if (arg.kind === 'Input') {
        let input = inputs[arg.index];
        if ('value' in input) {
          input = input.value;
        }
        if ('Object' in input && isImmOrOwnedObj(input.Object)) {
          inputObjects.push(input.Object.ImmOrOwned);
        }
      }
    });

    return inputObjects;
  }
}
